Return to the table of contents
By the end of this chapter, you should
class that uses pointers.
string class that is useful in some real programming situations.
operator >> and operator <<) to handle the objects of our string class.
memcmp and memset.
friend declaration, which allows access to private members by selected nonmember functions.
Now we're finally ready to examine exactly why the code for our operator = needs a reference argument rather than a value argument. I've drawn two diagrams that illustrate the difference between a value argument and a reference argument. First, Figure callbyvalue illustrates what happens when we call a function with a value argument of type string using the compiler-generated copy constructor.1
Caller's string n
Address Name string n +--------------------+ 12340000 m_Length | 0005 | +--------------------+ 12340002 m_Data | 1234febc +-+ +--------------------+ | | | +--------+ | 1234febc (none) | "T" |±------------+ +--------+ | +--------+ | 1234febd (none) | "e" | | +--------+ | +--------+ | 1234febe (none) | "s" | | +--------+ | +--------+ | 1234febf (none) | "t" | | +--------+ | +--------+ | 1234fec0 (none) | 0 | | +--------+ | | Called routine's string Str | +--------------------+ | 12340020 m_Length | 0005 | | +--------------------+ | 12340022 m_Data | 1234febc +-+ +--------------------+
In other words, with a value argument, the called routine makes a copy of the argument on its stack. This won't work properly with a string argument; instead, it will destroy the value of the caller's variable upon return to the calling function. Why is this?
The problem occurs when the destructor is called at the end of a function's execution to dispose of the copy that was made of the input argument during the function call. Since the copy points to the same data as the caller's original variable, the destruction of the copy causes the memory allocated to the caller's variable to be freed prematurely.
This is due to the way in which a variable is copied in C++ by the compiler-generated copy constructor. This mechanism for variable copying uses the same basic approach as the compiler-generated operator =; it makes a copy of all of the parts of the variable (a so-called memberwise copy). In the case of our string variable, this results in copying only the length m_Length and the pointer m_Data, and not the data that m_Data points to. That is, both the original and the copy refer to the same data, as indicated by Figure callbyvalue. If we were to implement our operator = with a string argument rather than a string&, then the following sequence of events would take place during the execution of the statement s = n;:
n, so that the variable Str in the operator = code would point to the same data as the caller's variable n.
Str variable would be used in the operator = code.
Str variable would be destroyed at the end of the operator = function. During this process, the destructor would free the memory that Str.m_Data points to by calling delete [].
Since Str.m_Data holds the same address as the caller's variable n.m_Data, the latter now points to memory that has been freed and may be overwritten or assigned to some other use at any time. This is a bug in the program caused by the string destructor being called for a temporary copy of a string that shares data with a caller's variable. When we use a reference argument, however, the variable in the called function is nothing more (and nothing less) than another name for the caller's variable. No copy is made on entry to the operator = code; therefore, the destructor is not called on exit. This allows the caller's variable n to remain unmolested after operator = finishes executing.
That may sound good, but Susan wanted some more explanation.
Susan: I don't get why a value argument makes a copy and a reference argument doesn't. Help.Figure callbyreference helped her out a bit by illustrating the same call as the one in Figure callbyvalue, but using a reference argument instead of a value argument.
Steve: The reason is that a value argument is actually a new
autovariable, just like a regularautovariable, except that it is initialized to the value of the caller's actual argument. Therefore, it has to be destroyed when the called function ends. On the other hand, a reference argument just renames the caller's variable; since the compiler hasn't created a newautovariable when the called routine starts, it doesn't need to call the destructor to destroy that variable at the end of the routine.
Caller's string n and called routine's string Str Address Name +--------------------+ 12340000 m_Length | 0005 | +--------------------+ 12340002 m_Data | 1234febc +-+ +--------------------+ | | +--------+ | 1234febc (none) | "T" |±------------+ +--------+ +--------+ 1234febd (none) | "e" | +--------+ +--------+ 1234febe (none) | "s" | +--------+ +--------+ 1234febf (none) | "t" | +--------+ +--------+ 1234fec0 (none) | 0 | +--------+
Finally, we're finished examining the intricacies that result from the apparently simple statement s = n; in our test program (Figure strassignx).
code/strtst1.cc
string class (code\strtst1.cc)
(Figure strassignx) Now let's take a look at the next statement in that test program, n = "My name is: ";. The type of the expression "My name is: " is char*; that is, the compiler stores the character data somewhere and provides a pointer to it. In other words, this line is attempting to assign a char* to a string. Although the compiler has no built-in knowledge of how to do this, we don't have to write any more code to handle this situation, because the code we've already written is sufficient. That's because if we supply a value of type char* where a string is needed, the constructor string::string(char*) is automatically invoked, much as the default constructor is invoked when we create a string with no arguments. Such automatic conversion is another of the features of C++ that makes our user defined types more like native types.2 The sequence of events during compilation of the line n = "My name is: "; is something like this:
string on the left of an =, which it interprets as a call to some version of string::operator =.
= and sees that it is not a string, but a char*.
string::operator = (char*)? If so, use it.
string::string(char*) for the string class.
n.operator = (string("My name is: "));. If there were no such constructor, then the line would be flagged as an error.
So the actual interpretation of n = "My name is: "; is n.operator = (string("My name is: "));. What exactly does this do?
Figure strnchar is a picture intended to illuminate the compiler's "thoughts" in this situation; that is, when we assign a C string with the value My name is: " to a string called n via the constructor string::string(char*).3
Address Name Contents
Step 1: Create temporary string from C string
C string +--------------------+ 12330000 (none) | "My name is: " | +---------+----------+ compiler generates | temporary via string::string(char*)| | Temporary string +--------------------+ 12340020 m_Length | 000d | +--------------------+ 12340022 m_Data | 12345600 +-+ +--------------------+ | | +--------------------+ | 12345600 (none) | "My name is: " |±+ +--------------------+
Step 2: Use operator = to assign value of temporary to n
string n +--------------------+ 12340120 m_Length | 000d | +--------------------+ 12340122 m_Data | 12345700 +-+ +--------------------+ | | +--------------------+ | 12345700 (none) | "My name is: " |±+ +--------------------+
Step 3: Call destructor for temporary string
Former temporary string +--------------------+ 12340020 (none) | ???? | +--------------------+ 12340022 (none) | ???????? | +--------------------+ +--------------------+ 12345600 (none) | ????????????? | +--------------------+
string via string::string(char*)
(Figure strnchar)
Let's go over Figure strnchar, step by step. The first thing that the compiler does is to call the constructor string::string(char*) to create a temporary (jargon for temporary variable) of type string, having the value "My name is: ". This temporary is then used as the argument to the function string::operator = (const string& Str) (see Figure stringassignment). Since the argument is a reference, no copy is made of the temporary; the variable Str in the operator = code actually refers to the (unnamed) temporary variable. When the operator = code is finished executing, the string n has been set to the same value as the temporary (i.e., "My name is: "). Upon return from the operator = code, the temporary is automatically destroyed by a destructor call inserted by the compiler.
This sequence of events also holds the key to understanding why the argument of string::operator = should be a const string& (that is, a constant reference to a string) rather than just a string& (that is, a reference to a string). You see, if we declared the function string::operator = to have a string& argument rather than a const string& argument, then it would be possible for that function to change the value of the argument. However, any attempt to change the caller's argument wouldn't work properly if, as in the current example, the argument turned out to be a temporary string constructed from the original argument (the char* value "My name is: "); clearly, changing the temporary string would have no effect on the original argument. Therefore, if the argument to string::operator = were a string&, the line n = "My name is: "; would produce a compiler warning to the effect that we might be trying to alter an argument that was a temporary value. The reason that we don't get this warning is that the compiler knows that we aren't going to try to modify the value of an argument that has the specification const string&; therefore, constructing a temporary value and passing it to string::operator = is guaranteed to have the behavior that we want.4 This example is anything but intuitively obvious and, as you might imagine, led to an extended discussion with Susan.
Susan: So no copy of the argument is made, but the temporary is a copy of the variable to that argument?
Steve: The temporary is an unnamed
stringcreated from the C string that was passed tooperator =by the statementn = "My name is: ";.
Susan: Okay. But tell me this: Is the use of a temporary the result of specifying a reference argument? If so, then why don't you discuss this when you first discuss reference arguments?
Steve: It's not exactly because we're using a reference argument. When a function is called with the "wrong" type of argument but a constructor is available to make the "right" type of argument from the "wrong" one that was supplied, then the compiler will supply the conversion automatically. In the case of calling
operator =with achar*rather than astring, there is a constructor that can make astringfrom achar*; so, the compiler will use that constructor to make a temporarystringout of the suppliedchar*and use that temporarystringas the actual argument to the functionoperator =. However, if the argument type were specified as astring&rather than aconst string&, then the compiler would warn us that we might be trying to change the temporarystringthat it had constructed; since we have aconst string&argument, the compiler knows that we won't try to change that temporarystring, so it doesn't need to warn us about this possibility.
Susan: Well, I never looked at it that way, I just felt that if there is a constructor for the argument then it is an OK argument.
Steve: As long as the actual argument matches the type that the constructor expects, there is no problem.
Susan: So, if the argument type were a
string&and we changed the temporary argument, what would happen? I don't see the problem with changing something that was temporary; I see that it would be a problem for the original argument but not the temporary.
Steve: The reason why generating a temporary is acceptable in this situation is that the argument is a
constreference. If we didn't add theconstin front of the argument specifier, then the compiler would warn us about our possibly trying to modify the temporary. Since we have aconstreference, the compiler knows that we won't try to modify the argument, and thus it's safe for the compiler to generate the temporary value.
Susan: OK, then the temporary is created any time you call a reference argument? I thought that the whole point of a temporary was so you could modify it and not the original argument and the purpose of the
constwas to ensure that would be the case.
Steve: The point is precisely that nothing would happen to the original argument if we changed the copy. Since one of the reasons that reference arguments are available is to allow changing of the caller's argument, the compiler warns us if we appear to be interested in doing that (a non-
constreference argument) in a situation where such a change would have no effect because the actual argument is a temporary.
Susan: So, if we have a non-
conststring&argument specification with an actual argument of typechar*then a temporary is made that can be changed (without affecting the original argument). If the argument is specified as aconst string&and the actual argument is of typechar*then a temporary is made that cannot be changed.
Steve: You've correctly covered the cases where a temporary is necessary, but haven't mentioned the other cases. Here is the whole truth and nothing but the truth:
- If we specify the argument type as
string&and a temporary has to be created because the actual argument is achar*rather than astring, then the compiler will warn us that changes to that temporary would not affect the original argument.- If we specify the argument type as
const string&and a temporary has to be created because the actual argument is achar*, then the compiler won't warn us that our (hypothetical) change would be ineffective, because it knows that we aren't going to make such a change.- However, if the actual argument is a
string, then no temporary needs to be made in either of these cases (string&orconst string&. Therefore, the argument that we see in the function is actually the real argument, not a temporary, and the compiler won't warn us about trying to change a (nonexistent) temporary.Susan: OK, this clears up another confusion I believe, because I was getting confused with the notion of creating a temporary that is basically a copy but I remember that you said that a reference argument doesn't make a copy; it just renames the original argument. So that would be the case in 3 here, but the temporary is called into action only when you have a situation such as in 1 or 2, where a reference to a
stringis specified as the argument type in the function declaration, while the actual argument is achar*.Steve: Right.
Assuming you've followed this so far, you might have noticed one loose end. What if we want to pass a string as a value argument to a function? As we have seen, with the current setup bad things will happen, since the compiler-generated copy constructor doesn't copy strings correctly. Well, you'll be relieved to learn that this, too, can be fixed. The answer is to implement our own version of the copy constructor, created precisely to solve the problem of copying variables of a given class, in this case string. Let's take another look at the header file, now in Figure string1hx.
code/string1.h
string class interface (code\string1.h)
(Figure string1hx) The line we're interested in here is string(const string& Str);. This is a constructor, since its name is the class name string. It takes one argument, which is a const string&, that is, a reference to a constant string. This means that we're not going to change the argument's value "through" the reference, as we could do via a non-const reference. The code in Figure stringcopycons implements this new constructor.
string::string(const string& Str): m_Length(Str.m_Length),
m_Data(new char [m_Length])
{
memcpy(m_Data,Str.m_Data,m_Length);
}
string class
(Figure stringcopycons) This function's job is similar to that of operator =, since both of these functions are in the copying business. However, there are also some differences; otherwise, we wouldn't need two separate functions.
Of course, the first difference is that because a copy constructor is a constructor, we can use a member initialization list to initialize the member variables; this convenience is not available to operator =, as it is not a constructor.
The next difference is that we don't have to check whether the argument refers to the same string as this. That's because a constructor always creates a new, never before seen, object of whatever class it's a constructor for. There's no equivalent of the statement a = a; that can cause trouble for operator = without special handling.
Finally, we don't have to delete any previously held storage that might have been assigned to m_Data. Of course, this is also because we're building a new string, not reusing one that already exists. Therefore, we know that m_Data has never had any storage assigned to it previously. One fine point that might have slipped past you is why we can't use a value argument rather than a reference argument to our copy constructor. The reason is that using a value argument of a class type requires a copy of the actual argument to be made, using a ... copy constructor! Obviously this won't work when we're writing the copy constructor for that type, and the compiler will let us know if we try to do this accidentally.
Now that we have a correct copy constructor, we can use a string as a value argument to a function, and the copy that's made by the compiler when execution of the function starts will be an independent string, not connected to the caller's variable. When this copy is destroyed at the end of the function, it will go away quietly and the caller's original variable won't be disturbed. This is all very well in theory, but it's time to see some practice. Let's write a function that we can call with a string to do some useful work, like displaying the characters in the string on the screen.
As I hope you remember from the previous chapters, we can send output to the screen via cout, a predefined output destination. For example, to write the character 'a' to the screen we could use the statement cout << 'a';. Although we have previously used cout and << to display string variables, our current version of the string class doesn't support them. If we want this ability, we'll have to provide it ourselves. Since variables that can't be displayed are limited in usefulness, we're going to start to do just that right now. Figure stringinterface3 is the updated header file.
I strongly recommend that you print out the files that contain this interface and its implementation, as well as the test program, for reference as you are going through this part of the chapter; those files are string3.h, string3.cc, and strtst3.cc, respectively.
code/string3.h
string class interface, with Display function (code\string3.h)
(Figure stringinterface3) As you can see, the new function is declared as void Display();. This means that it returns no value, its name is Display, and it takes no arguments. This last characteristic may seem odd at first, because surely Display needs to know which string we want to display. However, as we've already seen, each object has its own copy of all of the variables defined in the class interface. In this case, the data that are to be displayed are the characters pointed to by m_Data.
Figure stringtest3 is an example of how Display can be used.
code/strtst3.cc
string class test program, using the Display function (code\strtst3.cc)
(Figure stringtest3) See the line that says, n.Display();? That is how our new Display function is called. Remember, it's a member function, so it is always called with respect to a particular string variable; in this case, that variable is n.
As is often the case, Susan thought she didn't understand this idea, but actually did.
Susan: This Display stuff. . . I don't get it. Are you having to write also the information that theclasses need to display information on the screen?
Steve: Yes.
Now let's look at the implementation of this new member function, in Figure string3.
void string::Display(){
short i;
for (i = 0; i < m_Length-1; i ++)
cout << m_Data[i];
}
string class implementation of the Display function
(Figure string3) This should be looking almost sensible by now; here's the play by play. We start out with the function declaration, which says we're defining a void function (i.e., one that returns no value) that is a member function of the class string. This function is named Display, and it takes no arguments. Then we define a short called i. The main part of the function is a for loop, which is executed with the index starting at 0 and continuing while the index is less than the number of displayable characters that we use to store the value of our string; of course, we don't need to display the null byte at the end of the string. So far, so good. Now comes the tricky part. The next statement, which is the controlled block of the for loop, says to send something to cout; that is, display it on the screen. This makes sense, because after all that's the purpose of this function. But what is it that is being sent to cout?
It's just a char but that may not be obvious from the way it's written. This expression m_Data[i] looks just like a vector element, doesn't it? In fact, m_Data[i] is an element, but not of a vector. Instead, it's an element of an array, the C equivalent of a C++ vector. What's an array? Well, it's a bunch of data items (elements) of the same type; in this case, it's an array of chars. The array name, m_Data in this case, corresponds to the address of the first of these elements; the other elements follow the first one immediately in memory. If this sounds familiar, it should; it's very much like Susan's old nemesis, a pointer. However, like a vector, we can also refer to the individual elements by their indexes; so, m_Data[i] refers to the ith element of the array, which in the case of a char array is, oddly enough, a char.
So now it should be clear that each time through the loop, we're sending out the ith element of the array of chars where we stored our string data.
Susan and I had quite a discussion about this topic, and here it is.
Susan: And if this is an array or aThat's all very well, but where did that array come from? We definedvector(I can't tell which), how does theclassknow about it if you haven't written a constructor for it?
Steve: Arrays are a native feature of C++, left over from C. Thus, we don't have to (and can't) create a constructor and the like for them.
Susan: The best I can figure out from this discussion is that an array is like a
vectorbut instead of numbers it indexeschardata and uses a pointer to do it?
Steve: Very close. It's just like a
vectorexcept that it's missing some rather important features of avector. The most important one from our perspective is that an array doesn't have any error-checking; if you give it a silly index, you'll get something back, but exactly what is hard to determine.
Susan: If it is just like a
vectorand it is not as useful as a realvector, then why use it? What can it do that avectorcan't?
Steve: A
vectorisn't a native data type, whereas an array is. Therefore, you can use arrays to makevectors, which is in fact how I did it. We also wouldn't want to usevectors to hold ourstringdata because they're much more "expensive" (i.e., large and slow) to use. I'm trying to illustrate how we could make astringclassthat would resemble one that would actually be usable in a real program, and although simplicity is important, I didn't want to go off the deep end in hiding arrays from the reader.
Susan: So when you say that "we're sending out the
ith element of the array ofchars where we stored our value." does that mean that the "ith" element would be the pointer to some memory address where char data is stored?
Steve: No, the
ith element of the array is just like theith element of avector. If we had avectorof fourchars called x, we'd declare it as follows:
vector<char> x(4);
Then we could refer to the individual
chars in thatvectorasx[0],x[1],x[2], orx[3]. It's much the same for an array. If we had an array of fourchars calledy, we'd declare it as follows:
char y[4];
Then we could refer to the individual
chars in that array asy[0],y[1],y[2], ory[3].
m_Data as a char*, which is a pointer to (i.e., the address of) a char. As is common in C and C++, this particular char* is the first of a bunch of chars one after the other in memory.
Brace yourself for this one. In C++, a pointer and the address of an array are for almost all purposes the same thing. You can treat an array address as a pointer and a pointer as an array address, pretty much as you please. This is a holdover from C, necessary for compatibility with C programs. People who like C will tell you how "flexible" the equivalence of pointers and arrays in C is. That's true, but it's also extremely dangerous, because arrays have no error checking whatsoever. You can use whatever index you feel like, and whatever happens to be at the address that that index would have corresponded to will be your target of opportunity. The program in Figure char1 is an example of what can go wrong when using arrays.
code/dangchar.cc
characters (code\dangchar.cc)
(Figure char1) Let's look at what this program does when it's executed. First, we define three variables High, Middle, and Low, each as an array of 10 chars. Then we define a variable Alias as a char*; as you may recall, this is how we specify a pointer to a char. Such a pointer is essentially equivalent to a plain old memory address.
In the next part of the program, we use a for loop to set each element of the arrays High, Middle, and Low to a value. So far, so good, except that the statement Middle[i] = 'A' + i; may look a bit odd. How can we add a char value like 'A' and a short value such as i?
Let us return to those thrilling days of yesteryear, or at least Chapter basics.htm. Since then, we've been using chars to hold ASCII values, which is their most common use. However, every char variable actually has a "double life"; it can also be thought of as a "really short" numeric variable, which can take on any of 256 values. Thus, we can add and subtract chars and shorts, as long as we're careful not to try to use a char to hold a number greater than 255 (or greater than 127, for a signed char). In this case, there's no problem with the magnitude of the result, since we're starting out with the value A and adding a number between 0 and 9 to it; the highest possible result is J, which is still well below the maximum value that can be stored in a char. With that detail taken care of, let's proceed with the analysis of this program. The next statement after the end of the first for loop is the seemingly simple line alias = middle;. This is obviously an assignment statement, but what is being assigned?
The value that Alias receives is the address of the first element of the array Middle. That is, after the assignment statement is executed, Alias is effectively another name for Middle. Therefore, the next loop, which assigns values to elements 10 through 19 of the "array" Alias, actually operates on the array Middle, setting those elements to the values k through t.
The rest of the program is pretty simple. It just displays the characters from each of the Low, Middle, Alias, and High arrays. Of course, Alias isn't really an array, but it acts just like one; to be precise, it acts just like Middle, since it points to the first character in Middle. Therefore, the Alias and Middle loops will display the same characters. Then the final loop displays the values in the High array.
That's pretty simple, isn't it? Not quite as simple as it looks. If you've been following along closely, you're probably thinking I've gone off the deep end. First, I said that the array Middle had 10 elements (which are numbered 0 through 9, as always in C++); now I'm assigning values to elements numbered 10 through 19. Am I nuts?
No, but the program is. When you run it, you'll discover that it produces output similar to Figure char2.
code/dangchar.out
Most of these results are pretty reasonable: Low is just as it was when we initialized it, and Middle and Alias have the expected portion of the alphabet. But look at High. Shouldn't it be all 0s?
Yes, it should. However, we have broken the rules by writing "past the end" of an array, and the result is that we have overwritten some other data in our program, which in this case turned out to be the original values of High. You may wonder why we didn't get an error message, as we did when we tried to write to a nonexistent vector element in an earlier chapter. The reason is that, in C, an array is no more (and no less) than the address of some data. In other words, it's just like a pointer, except that the address it refers to can't be changed at run time.
In case this equivalence of arrays and pointers isn't immediately obvious, don't worry; it wasn't obvious to Susan, either.
Susan: And when you say that "that is, a pointer (i.e., the address of) aBecause of this near-identity between pointers and arrays, the compiler does not and cannot keep track of how many elements are in an array. To the compiler, keeping track of such information makes no sense, any more than it can tell how many elements there are "in a pointer". This is why pointers and (equivalently) arrays are the single most error-prone construct in C (and C++, when they're used recklessly). It's also why we're not going to use either of these constructs except when there's no other reasonable way to accomplish our goals; even then, we'll confine them to tightly controlled circumstances in the implementation of a user defined data type. For example, we don't have to worry about going "off the end" of the array in ourchar, which may be the first of a bunch ofchars one after another in memory", does that mean thechar*points to the first address and then the second and then the third individually, and an array will point at all of them at the same time?
Steve: No, the
char*points to the firstchar, but we can figure out the addresses of the otherchars because they follow the firstcharsequentially in memory. The same is true of the array; the array name refers to the address of the firstchar, and the otherchars in the array can be addressed with the index added to the array name. In other words,y[2]in the example means "thecharthat is 2 bytes past the beginning of the array calledy".
Display function, because we know exactly how many characters we've stored (m_Length), and we've written the function to send exactly that many characters to the screen via cout. In fact, all of the member functions of our string class are carefully designed to allocate, use, and dispose of the memory pointed to by m_Data so that the user of this class doesn't have to worry about pointers or arrays, or the problems they can cause. After all, one of the main benefits of using C++ is that the users of a class don't have to concern themselves with the way it works, just with what it does.
Assuming that you've installed the software from the CD-ROM in the back of this book, you can try out this program. First, you have to compile it by changing to the code subdirectory under the main directory where you installed the software, and typing RHIDE dangchar, then using the "Make" command from the "Compile" menu. Then exit back to DOS and type dangchar to run the program. To run it under the debugger, make sure you are in the code subdirectory, and then type "RHIDE dangchar". Again, do *not* add the ".cc" to the end of the file name. Once RHIDE has started up, you can step through the program by hitting F8, which will treat any function call as one statement, or by hitting F7, which will step into any function call. Any time you want to see the display that the user would see when running the program normally, hit Alt-F5, then ENTER to get back to the debugger.
Susan and I had quite a discussion about this program:
Susan: It is still not clear to me why you assigned values to elements numbered 10-19. Was that for demonstration purposes to force "writing past the end"?
Steve: Yes.
Susan: So by doing this to
Middlethen it alters the place the pointer is going to point forHigh?
Steve: No, it actually uses some of the same addresses that
Highuses, with the result being. . . well, do you remember the functionmess?
Susan: Oh, don't remind me. So then when
Highruns there isn't any memory to put its results in? Why doesMiddleoverwriteHighinstead ofHighoverwriteMiddle? But it wasAliasthat took uphigh's memory?
Steve: Actually
Highis filled up with the correct values, but then they're overwritten by the loop that stores viaAlias, which is just another name forMiddle. I'll explain that in detail.
Susan: Is that why
Hightook on the lower case letters? BecauseMiddletook the first loop and thenAliasis the same as middle, so that is why it also has the upper case letters but then whenHighlooped it picked up the pointer whereAliasleft off and that is why it is in lower case? But how did it manage two zeros at the end? I better stop talking, this is getting too weird. You are going to think I am nuts.
Steve: No. You're not nuts, but the program is. We're breaking the rules by writing "past the end" of the array
Middle, using the pointerAliasto do so. We could have gotten the same result by storing data into elements 10 through 20 ofMiddle, but I wanted to show the equivalence of pointers and arrays.
Susan: Oops, I got ahead of myself, and yes, I was just going to ask if
Middlealone would have been sufficient to do the job.
Steve: Yes, it would have been. However, it would not have made the point that arrays and pointers are almost identical in C++.
Susan: I am confused as to why the lower case letters are in
Highand whykandlare missing and two zeros made their way in. You never told me why those zeros were there.
Steve: Because the end of one array isn't necessarily immediately followed by the beginning of the next array; this depends on the sizes of the arrays and on how the compiler allocates local variables on the stack. What we're doing here is breaking the rules of the language, so it shouldn't be a surprise that the result isn't very sensible.
Susan: OK, so if you are breaking the rules you can't predict an outcome? Here I was trying to figure out what was going on by looking at the results even knowing it was erroneous. Ugh.
Steve: Indeed.
private Property: Keep Out!Now that we have disposed of the correspondence between arrays and pointers, it's time to return to our discussion of the private access specifier that we've used to control access to the member variables of the class. First of all, let me refresh your memory as to what this access specifier means: only member functions of the string class can refer to variables or functions marked private. As a rule, no member variables of a class should be public, for reasons that we'll get into later. By contrast, most member functions are public, because such functions provide the interface that is used by programmers who need the facilities of the class being defined. While private member functions are sometimes useful for handling implementation details that aren't of interest or use to the "outside world" beyond the class boundaries, they aren't needed in most classes, including the classes defined in this book.
Now that I've clarified the role of these access specifiers, let's take a look at the program in Figure private, which won't compile because it tries to refer to m_Length, a private member variable of string.
code/strtst3a.cc
Here's the result of trying to compile this program:
strtst3a.cc: In function `int main()':
strtst3a.cc:8: member `m_Length' is a private member of
class `string'
Why would we want to prevent access to a member variable? Because public member variables have problems similar to those of global variables. To begin with, we want to guarantee consistent, safe behavior of our strings, which is impossible if a nonmember function outside our control can change one of our variables. In the example program, assigning a new value to the m_Length member variable would trick our Display member function into trying to display 12 characters, when our string contains only four characters of displayable data. Similar bad results would occur if a nonmember function were to change the value of m_Data; we wouldn't have any idea of what it was pointing to or whether we should call delete in the destructor to allow the memory formerly used for our string data to be reused. Of course, Susan had some questions about this notion of access restriction.
Susan: Now let me see if I get the problem here with this Figure private. When you originally wroteWhile this may be a convincing argument against letting nonmember functions change our member variables, what about letting them at least retrieve the values of member variables? Surely that must be harmless.m_Length, it was placed in aprivatearea, so it couldn't be accessed through this program?
Steve: Right.
Susan: I am confused on your use of the term nonmember function. Does that mean a nonmember of a particular
classor something that is native? For this reason I am not understanding the first paragraph.
Steve: A nonmember function is any function that is not a member of the
classin question.
Susan: A simple concept, but one that is easy to forget for some reason.
Steve: Probably because it's stated negatively.
Unfortunately this would be hazardous too. The problem here is akin to the other difficulty with global variables: Removing or changing the type of a global variable can cause repercussions everywhere in the program. If we decide to implement our string class by a different mechanism than a char* and a short, or even change the names of the member variables from m_Data and m_Length, any programs that rely on those types or names would have to be changed. If our string class were to become popular, this might amount to dozens or even hundreds of programs that would need to be changed if we were to make the slightest change in our member variables. Therefore, allowing nonmember functions even to retrieve the values of member variables is dangerous to the maintainability of programs using a class.
However, it is sometimes useful for a program that is using an object to find out something about the object's internal state. For example, a user of a string variable might very well want to know how many characters it is storing at the moment, for example when formatting a report. Each string might require a different amount of padding to make the columns on the report line up, depending on the number of visible characters in the string; however, we don't want the length the user sees to include the null byte, which doesn't take up any space on the page. Susan wanted to know how we could allow the user to determine the string's length.
Susan: So how would you "fix" this so that it would run? If you don't want to changeAs I've just mentioned to Susan, it is indeed possible to provide such a service without compromising the safety or maintainability of ourm_Lengthto somethingpublic, then would you have to rewrite anotherstringclassfor this program?
Steve: No, you would generally fix this by writing a member function that returns the length of the
string; theGetLengthfunction to be implemented in Figure string4 is an example of that.
Susan: Oh, so then
m_LengthstaysprivatebutGetLengthispublic?
Steve: Exactly.
class by writing a function that tells the user how long the string is. Figure stringinterface4 has the new interface definition that includes the GetLength function.
I strongly recommend that you print out the files that contain this interface and its implementation, as well as the test program, for reference as you are going through this part of the chapter; those files are string4.h, string4.cc, and strtst4.cc, respectively.
code/string4.h
string class interface (code\string4.h)
(Figure stringinterface4) As you can see, all we have to do here is to add the declaration of the new function, GetLength. The implementation in Figure string4 is extremely simple: it merely returns the number of chars in the string, deducting 1 for the null byte at the end.
short string::GetLength(){
return m_Length-1;
}
string class implementation of the GetLength function (from code\string4.cc)
(Figure string4) This solves the problem of letting the user of a string variable find out how long the string is without allowing functions outside the class to become dependent on our implementation. It's also a good example of how we can provide the right information to the user more easily by creating an access function rather than letting the user get at our member variables directly; after all, we know that m_Length includes the null byte at the end of the string's data, which is irrelevant to the user of the string, so we can adjust our return value to indicate the "visible length" rather than the actual one.
With this mechanism in place, we can make whatever changes we like in how we store the length of our string;5 as long as we don't change the name or return type of GetLength, no function outside the string class would be affected.6 The example program in Figure strtst4 illustrates how to use this new function.
code/strtst4.cc
GetLength function in the string class (code\strtst4.cc)
(Figure strtst4)
After finishing most of the requirements to make the string class a concrete data type in the previous chapter, we went back to look at why the operator = needs a reference argument rather than a value argument. When we use a value argument, a copy of the argument is made. In the case of a user-defined data type, this copy is made via the copy constructor defined for that type. If we don't define our own copy constructor, the compiler will generate one for us, which will use memberwise copy; that is, simply copying all of the member variables in the object. While a memberwise copy is fine for simple objects whose data are wholly contained with themselves, it isn't sufficient for objects that contain pointers to data stored in other places, because copying a pointer from one object to another results in the two objects sharing the same actual data. Since our string class does contain such a pointer, the result of this simple(minded) copy is that the newly created string points to the same data as the caller's string. Therefore, when the newly created local string expires at the end of the operator = function, the destructor for that string frees the memory that the caller's string was using to store its data.
This problem is very similar to the reason why we had to write our own operator = in the first place; the compiler-generated operator = just copies the member variables from the source to the destination object, which causes similar havoc when one of the two "twinned" strings is changed. In the case of our operator =, we can solve the twinning problem by using a reference argument rather than a value argument. A reference argument is actually another name for the caller's variable rather than a copy of the value in that variable, so no destructor is called for a reference argument when the function exits; therefore, the caller's variable is left unmolested.
Next, we examined how it was possible to assign a C string to one of our string variables. This didn't require us to write any more code because we already had a constructor that could create a string from a C string, and an operator = that could assign one string to another one. The compiler helps us out here by employing a rule that can be translated roughly as follows: If we need an object of type A (string, in this case) and we have an object of type B (char*, in this case), and there is a constructor that constructs an A and requires exactly one argument, which is of type B, then invoke that constructor automatically. The example code is as follows:
n = "My name is: ";
where n is a string, and "My name is" is a C string, whose type is char*. We have an operator = with the declaration:
string& string::operator = (const string& Str);
which takes a string reference argument, and we have a constructor of the form:
string::string(char* p);
which takes a char* argument and creates a new string. So we have a char*, "My name is: ", and we need a string. Since we have a constructor string::string(char*), the compiler will use that constructor to make a temporary string with the same value as the char*, and then use the assignment operator string::operator = (const string& Str) to assign the value of that temporary string to the string n. The fact that the temporary is created also provides the clue as to why the argument to string::operator = (const string& Str) should be a const reference, rather than just a (non-const) reference, to a string. The temporary string having the value "My name is: " created during the execution of the statement n = "My name is: "; disappears after operator = is executed, taking with it any changes that operator = might have wanted to apply to the original value. With a const reference, the compiler knows that operator = doesn't wish to change that argument, and therefore doesn't give us a warning that we might be changing a temporary value.
At this point, we've taken care of operator =. However, to create a concrete data type, we still have to allow our string variables to be passed as value arguments. Unfortunately, the compiler-generated copy constructor suffers from the same drawback as the compiler-generated operator =; namely, it copies the pointer to the actual data of the string, rather than the data itself. Logically, therefore, the solution to this problem is quite similar to the solution for operator =: We write our own copy constructor that allocates space for the character data to be stored in the newly created string and then copies the data from the old string to the new string.
However, we still can't use a value argument to our copy constructor, because a value argument needs a copy constructor to make the copy. This obviously won't work, and will be caught by the compiler. Therefore, as in the case of operator =, we have to use a reference argument; since this is actually just another name for the caller's variable rather than a copy of it, no destructor for the reference argument is called at exit from our copy constructor. Since we are not going to change the caller's argument, we specify a constant reference argument of type string, or a const string& in C++ terms.
At that point in the chapter, we had met the requirements for a concrete data type, but such a type is of limited usefulness as long as we can't get the values displayed on the screen. Therefore, the next order of business was to add a Display member function that takes care of this task. This function isn't particularly complicated, but it does require us to deal with the notion of a C legacy type, the array. Since the compiler treats an array in almost the same way as a pointer, we can use array notation to extract each character that needs to be sent out to the screen. Continuing with our example of the Display function's use, the next topic was a discussion of how chars can be treated as numeric variables.
Then we saw a demonstration of how easy it is to misuse an array so that you destroy data that belong to some other variable. This is an important warning of the dangers of uncontrolled use of pointers and arrays; these are the most error-prone constructs in both C and C++, when not kept under tight rein.
We continued by revisiting the topic of access control and why it is advantageous to keep member variables out of the public section of the class definition. The reasons are similar to those of using global variables; it's too hard to keep track of where the value of a public member variable is being referenced and therefore to update all the affected areas of the code when changing the class definition. However, it is sometimes useful to allow external functions access to some information about a class object. We saw how to do this by adding a GetLength member function to our string class.
At this point, we have a fairly minimal string class. We can create a string, assign it a literal value in the form of a C string, and copy the value of one string to another; we can even pass a string as a value argument. Now we'll use the techniques that we've already covered (along with others that we find necessary in the process) to improve the facilities that the string class provides.
To make this goal more concrete, let's suppose that we want to modify the sorting program of Chapter to sort strings, rather than shorts. To use the sorting algorithm from that program, we'll need to be able to compare two strings to see which would come after the other in the dictionary, as we can compare two shorts to see which is greater. We also want to be able to use cout and << to display strings on the screen, and cin and >> to read them from the keyboard, just as we can with native types.
Before we go into the changes needed in the string class to allow us to write a string sorting program, Figure strsort1 shows our goal: The selection sort algorithm adapted to sort a vector of strings rather than shorts.
Assuming that you've installed the software from the CD-ROM in the back of this book, you can try out this program. First, you have to compile it by changing to the code subdirectory under the main directory where you installed the software, and typing RHIDE strsort1, then using the "Make" command from the "Compile" menu. Then exit back to DOS and type strsort1 to run the program.
To run it under the debugger, make sure you are in the code subdirectory, and then type "RHIDE strsort1". Again, do *not* add the ".cc" to the end of the file name. Once RHIDE has started up, you can step through the program by hitting F8, which will treat any function call as one statement, or by hitting F7, which will step into any function call. Any time you want to see the display that the user would see when running the program normally, hit Alt-F5, then ENTER to get back to the debugger.
code/strsort1.cc
vector of strings (code\strsort1.cc)
(Figure strsort1)Susan had a couple of comments and questions about this program:
Susan: OK, I have this figured out now. I mentioned to you a long time ago that I was confused with the code here when you ask for last names but you are using a variableAs you can see, this program looks very similar to the code that sortsFirstName. This is a little confusing to a novice because it seems that you don't know what you are asking for. Now I get it: you are asking for the first one of the last names.
Steve: Actually,
FirstNameis the "lowest" name that we've found so far in the current pass, sort of likeHighestWeightin the original program. I think we're saying the same thing here.
Susan: Why aren't you using caps when you initiate your variable of
HighestName; I don't understand why you use "zzzzzzzzzz" instead of "ZZZZZZZZZZ" ? Are you going to fix this later so that caps will work the same way as lower case letters?
Steve: If I were to make that change, the program wouldn't work correctly if someone typed their name in lower case letters, because lower case letters are higher in ASCII value than upper case letters. That is, "abc" is higher than "ZZZ". Thus, if someone typed in their name in lower case, the program would fail to find their name as the lowest name. Actually, the way the
stringsorting function works, "ABC" is completely different from "abc"; they won't be next to one another in the sorted list. We could fix this by using a different method of comparing thestrings that would effectively convert everything to upper case before sorting it, if that were necessary.
short values, which is good because that's what we wanted to achieve. Let's take a look at the differences between this program and the original one in Figure vect1.
vectors Name and SortedName are collections of strings, rather than the corresponding vectors of shorts in the first program: Weight and SortedWeight.
HighestName, which plays the role of the value 0 that was used to initialize HighestWeight in the original program; that is, it is used to initialize the variable FirstName to a value that will certainly be replaced by the first name we find, just as 0 was used to initialize the variable HighestWeight to a value that had to be lower than the first weight we would find. The reason why we need a "really high" name rather than a "really low" one is because we're sorting the "lowest" name to the front, rather than sorting the "highest" weight to the front as we did originally.
You may think these changes to the program aren't very significant. That's a correct conclusion; we'll spend much more time on the changes we have to make to our string class before this program will run, or even compile. The advantage of making up our own data types (like strings) is that we can make them behave in any way we like. Of course, the corresponding disadvantage is that we have to provide the code to implement that behavior and give the compiler enough information to use that code to perform the operations we request. In this case, we'll need to tell the compiler how to compare strings, read them in via >> and write them out via <<. Let's start with Figure friend1, which shows the new interface specification of the string class, including all of the new member functions needed to implement the comparison and I/O operators, as well as operator ==, which we'll implement later in the chapter.
code/string5.h
string class interface, including comparison and I/O operators (code\string5.h)
(Figure friend1) I strongly recommend that you print out the files that contain this interface and its implementation, as well as the test program, for reference as you are going through this part of the chapter; those files are string5.h, string5.cc, and strtst5.cc, respectively.
Let's start by implementing operator < (the "less than" operator) so that we can use the selection sort to arrange strings by their dictionary order. The signature of this operator is similar to that of operator =, except that rather than defining what it means to say x = y; for two strings x and y, we are defining what it means to say x < y. Of course, we want our operator < to act analogously to the < operator for short values; that is, our operator will compare two strings and return true if the first string would come before the second string in the dictionary and false otherwise. That's all we need for the selection sort.
All right, then, how do we actually implement this undoubtedly useful facility? Let's start by examining the function declaration bool string::operator < (const string& Str); a little more closely. This means that we're declaring a function that returns a bool and is a member function of class string; its name is operator <, and it takes a constant reference to a string as its argument. As we've seen before, operators don't look the same when we use them as when we define them. In the sorting program in Figure strsort1, the line if (Name[k] < FirstName) actually means if (Name[k].operator < (FirstName)). In other words, if the return value from the call to operator < is false, then the if expression will also be considered false and the controlled block after the if won't be executed. On the other hand, if the return value from the call to operator < is true, then the if expression will also be considered true and the controlled block of the if will be executed. To make this work correctly, our version of operator < will return the value true if the first string is less than the second string and the value false otherwise.
Now that we've seen how the compiler will use our new function, let's see how it does its work. The basic approach we will use to compare two strings is as follows:
strings.
string with the corresponding character from the second string.
string is less than the character from the second string, then we know that the first string precedes the second in the dictionary, so we're done and the result is true.
string is greater than the character from the second string, then we know that the first string follows the second in the dictionary, so we're done and the result is false.
string, then move to the next character in each string, and go back to step 2.
strings are the same length, then the answer is that they are identical, so we're done and the result is false.
strings are different in length, and if we run out of characters in the shorter string before finding a difference between the two strings, then the longer string follows the shorter one in the dictionary. In this case, the result is true if the second string is longer and false if the first string is longer.
One question that might occur to you on looking over the preceding explanation is why we care whether two strings differ in length. Wouldn't it be simpler just to compare up to the length of the longer string?
As it happens, the comparison would work properly so long as both of the strings we're comparing have a null byte at their ends and neither of them has a null byte anywhere else. To see why this restriction is necessary, let's look at what the memory layout might look like for two string variables x and y, with the contents "post" and "poster" respectively. In Figure compare1, the letters in the box labeled "string contents" represent themselves, while the 0s represent the null byte, not the digit 0.
If we were to compare the strings up to the longer of the two lengths with this memory layout, the sequence of events would go like this:
string is higher than the null byte from the second string, so we conclude (correctly) that the first string comes after the second one.
This works because the null byte, having an ASCII code of 0, in fact is less than whatever non-null byte in the corresponding position of the other string.
Address Name string x +--------------------+ 12340000 m_Length | 0005 | +--------------------+ 12340002 m_Data | 12345607 +----+ +--------------------+ | string y | +--------------------+ | 12340020 m_Length | 0007 | | +--------------------+ | 12340022 m_Data | 12345600 +-+ | +--------------------+ | | | | string contents | | +--------------------+ | | 12345600 (none) | poster0 |±+ | +--------------------+ | 12345607 (none) | post0 |±---+ +--------------------+ 1234560c (none) | test | +--------------------+
strings x and y in memory
(Figure compare1) However, this plan wouldn't work reliably if we had a string with a null byte in the middle. To see why, let's change the memory layout slightly to stick a null byte in the middle of string y. Figure compare2 shows the modified layout.
You may reasonably object that we don't have any way to create a string with a null byte in it. That's true at the moment, but one reason we're storing the actual length of the string rather than relying on the null byte to mark the end of a string, as is done with C strings, is that keeping the length separately makes it possible to have a string that has any characters whatever in it, even nulls. For example, we could add a string constructor that takes an array of bytes and a length and copies the specified number of bytes from the array.
Address Name string x +--------------------+ 12340000 m_Length | 0005 | +--------------------+ 12340002 m_Data | 12345607 +----+ +--------------------+ | string y | +--------------------+ | 12340020 m_Length | 0007 | | +--------------------+ | 12340022 m_Data | 12345600 +-+ | +--------------------+ | | | | string contents | | +--------------------+ | | 12345600 (none) | post0r0 |±+ | +--------------------+ | 12345607 (none) | post0 |±---+ +--------------------+ 1234560c (none) | test | +--------------------+
strings x and y in memory, with an embedded null byte
(Figure compare2)Since an array of bytes can contain any characters in it, including nulls, that new constructor would obviously allow us to create a string with a null in the middle of it; if we tried to use the preceding comparison mechanism, it wouldn't work reliably, as shown in the following analysis.
string is less than the character t from the second string, so we conclude that the first string comes before the second one.
Unfortunately, this conclusion is completely fallacious; what we have actually done is run off the end of the second string and started retrieving data from the next location in memory. Since we want to be able to handle the situation where one of the strings has one or more embedded nulls, we have to stop the comparison as soon as we get to the end of the shorter string; whatever happens to be past the end of that string's data, it's not anything relevant to our comparison of the two strings.
Let's listen in on the conversation Susan and I had about this function.
Susan: Why is the return value fromNow that we've examined why the algorithm foroperator <abool?
Steve: Because it has to return a value indicating whether the first string is less than the second string. An
ifstatement using thisoperator <will work properly if the result of this function isfalsefor "not less than" andtruefor "less than". Thus, aboolis appropriate for this use.
Susan: Again I am not seeing where we're using
string::operator < (const string& Str);in the sorting program.
Steve: That's because all you have to say is
a < b, just as withoperator =; the compiler knows thata < b, whereaandbarestrings, meansstring::operator < (const string&).
Susan: Why are you bringing up this stuff about what the operator looks like and the way it is defined? Do you mean that is what is really happening behind the curtain even though it looks like built in code?
Steve: Yes.
Susan: How do those null bytes get into memory? Who puts them there?
Steve: The compiler supplies a null byte automatically at the end of every literal string, such as "abc".
Susan: I don't get where you say you are not using a null byte when storing the length. . . it looks to me that you are. This is confusing. Ugh.
Steve: I understand why that's confusing, I think. I am including the null byte at the end of a
stringwhen we create it from a literal C string, so that we can mix ourstrings with C strings more readily; however, because we store the length separately, it's possible to construct astringthat has null bytes in the middle of it as well as at the end. This is not possible with a C string, because that has no explicit length stored with it; instead, the routines that operate on C strings assume that when they get to a null byte, the C string is finished.
Susan: Why do you jump from a null byte to a t? Didn't it run out of letters? Is this what you mean by retrieving data from the next location in memory? Why was a t there?
Steve: Yes, this is an example of retrieving random information from the next location in memory. We got a t because that just happened to be there. The problem is that since we're using an explicit length rather than a null byte to indicate the end of our
strings, we can't count on a null byte stopping the comparison correctly. Thus, we have to worry about handling the case where there is a null byte in the middle of astring.
operator < works the way it does, it will probably be easier to understand the code if we follow an example of how it is used. I've written a program called strtst5x.cc for this purpose; Figure operator2 has the code for that program.
code/strtst5x.cc
operator < for strings (code\strtst5x.cc)
(Figure operator2) You can see that in this program the two strings being compared are "ape" and "axes", which are assigned to strings x and y respectively. As we've already discussed, the compiler translates a comparison between two strings into a call to the function string::operator <(const string& Str); in this case, the line that does that comparison is if (x < y).
Now that we've seen how to use this comparison operator, Figure operatorimp shows one way to implement it.
bool string::operator < (const string& Str){
short i;
bool Result;
bool ResultFound;
short CompareLength;
if (Str.m_Length < m_Length)
CompareLength = Str.m_Length;
else
CompareLength = m_Length;
ResultFound = false;
for (i = 0; (i < CompareLength) && (ResultFound == false); i ++)
{
if (m_Data[i] < Str.m_Data[i])
{
Result = true;
ResultFound = true;
}
else
{
if (m_Data[i] > Str.m_Data[i])
{
Result = false;
ResultFound = true;
}
}
}
if (ResultFound == false)
{
if (m_Length < Str.m_Length)
Result = true;
else
Result = false;
}
return Result;
}
operator < for strings (from code\string5a.cc)
(Figure operatorimp)The variables we'll use in this function are:
i, which is used as a loop counter in the for loop that steps through all of the characters to be compared.
Result, which is used to hold the true or false value that we'll return to the caller.
ResultFound, which we'll use to keep track of whether we've found the result yet.
CompareLength, which we'll use to determine the number of characters to compare in the two strings.
After variable definition, the next four lines of the code determine how many characters from each string we actually have to compare; the value of CompareLength is set to the lesser of the lengths of our string and the string referred to by Str. In this case, that value is 4, the length of our string (including the terminating null byte).
Now we're ready to do the comparison. This takes the form of a for loop that steps through all of the characters to be compared in each string. The header of the for loop is for (i = 0; (i < CompareLength) && (ResultFound == false); i ++). The first and last part of the expression controlling the for loop should be familiar by now; we're setting i, the index variable, to 0, and incrementing i each time through the loop. But what about the continuation expression (i < CompareLength) && (ResultFound == false)?
What we're doing here is specifying a two-part condition for continuing the loop. The first part, (i < CompareLength), is the usual condition that allows the program to execute the loop as long as the index variable is within the correct range. The second part, (ResultFound == false) should also be fairly clear; we want to test whether we've already found the result we're looking for, and continue only as long as that isn't the case (i.e., ResultFound is still false). The () around each of these expressions are used to tell the compiler that we want to evaluate each of these expressions first, before the && is applied to their results. That leaves the && symbol as the only mystery.
It's really not too mysterious. The && operator is the symbol for the "logical AND" operation, which means that we want to combine the truth or falsity of two expressions each of which has a logical value of true or false. The result of using && to combine the results of these two expressions will also be a logical value. Here is the way the value of that expression is determined:
&& are true, then the value of the expression containing the && is also true;
&& is false.
If you think about it for a minute, this should be comprehensible. We want to continue the loop as long as both of the conditions are true; that is,
i is less than CompareLength; and
ResultFound is false (i.e, we haven't found what we're looking for).
That's why the && operator is called logical AND; it checks whether condition 1 and condition 2 are both true. If either is false, we want to stop the loop, and this continuation expression will do just that.7 Now let's trace the path of execution through the for loop in Figure operatorimp. On the first time through the loop, the index i is 0 and ResultFound is false. Therefore, the continuation expression allows us to execute the statements in the loop, where we test whether the current character in the current string, m_Data[i], is less than the corresponding character from the string Str, Str.m_Data[i].
By the way, in case the expression in the if statement doesn't make sense immediately, perhaps I should remind you that the array notation m_Data[i] means the ith character of the data pointed to by m_Data; an index value of 0 means the first element, as is always the case when using an array. We've already covered this starting on page ; you should go back and reread that section if you're not comfortable with the equivalence between pointers and arrays.
The code in Figure operator3 compares the characters from the two strings.
if (m_Data[i] < Str.m_Data[i]){
Result = true;
ResultFound = true;
}
In the event that the current character in our string is indeed less than the corresponding character in Str, we have our answer: Our string is less than the other string. If that were the case, we would set Result to true and ResultFound to true, and we would be finished with this execution of the for loop.
As it happens, in our current example both m_Data[0] and Str.m_Data[0] are equal to a, so they're equal to each other as well. What happens when the character from our string is the same as the one from the string Str?
In that case, the first if, whose condition is stated as if (m_Data[i] < Str.m_Data[i]), is false. So we continue with the else clause of that if statement, which looks like Figure operator4.
else{
if (m_Data[i] > Str.m_Data[i])
{
Result = false;
ResultFound = true;
}
}
else clause in the comparison loop (from code\string5a.cc)
(Figure operator4) This clause contains another if statement that compares the character from our string to the one from Str. Since the two characters are the same, this if also comes out false, so the controlled block of the if isn't executed. After this if statement, we've reached the end of the controlled block of the for statement. The next iteration of the for loop starts by incrementing i to 1. Then the continuation expression is evaluated again; i is still less than CompareLength, and ResultFound is still false, so we execute the controlled block of the loop again with i equal to 1.
On this pass through the for loop, m_Data[1], the character from our string, is p, and Str.m_Data[1], the character from the other string, is x. Therefore, the condition in the first if statement (that the character from our string is less than the character from the other string) is true, so we execute the controlled block of the if statement; this sets Result to true, and ResultFound also to true, as you can see in Figure operator3.
We're now at the end of the for loop, so we return to the for statement to continue execution. First, i is incremented again, to 2. Then the continuation expression (i < CompareLength) && (ResultFound == false) is evaluated. The first part of the condition, i < CompareLength is true, since i is 2 and CompareLength is 4. However, the second part of the condition, ResultFound == false, is false, because we've just set ResultFound to true. Since the result of the && operator is true only when both subconditions are true, the for loop terminates, passing control to the next statement after the controlled block of the loop (Figure operator5).
if (ResultFound == false){
if (m_Length < Str.m_Length)
Result = true;
else
Result = false;
}
In the current scenario, ResultFound is true because we have found a character from m_Data that differs from the corresponding character from Str.m_Data; therefore, the condition in the first if is false, and we proceed to the next statement after the end of the if statement, return Result;. This shouldn't come as too much of a surprise; we know the answer to the comparison, namely, that our string is less than the other string, so we're ready to tell the caller the information that he requested by calling our routine.
The path of execution is almost exactly the same if, the first time we find a mismatch between the two strings, the character from our string is greater than the character from the other string. The only difference is that the if statement that handles this scenario sets Result to false rather than true (Figure operator4), because our string is not less than the other string; of course, it still sets ResultFound to true, since we know the result that will be returned.
There's only one other possibility: That the two strings are the same up to the length of the shorter one (e.g., "post" and "poster"). In that case, the for loop will expire of natural causes when i gets to be greater than or equal to CompareLength. Then the final if statement shown in Figure operator5 will evaluate to true, because ResultFound is still false. In this case, if the length of our string is less than the length of the other string, we will set Result to true, because a shorter string will precede a longer one in the dictionary if the two strings are the same up to the length of the shorter one.
Otherwise, we'll set Result to false, because our string is at least as long as the other one; since they're equal up to the length of the shorter one, our string can't precede the other string. In this case, either they're identical, or our string is longer than the other one and therefore should follow it. Either of these two conditions means that the result of operator < is false, so that's what we tell the caller via our return value.
This implementation of operator < for strings definitely works. However, there's a much simpler way to do it. Figure operator6 shows the code.
bool string::operator < (const string& Str){
short Result;
short CompareLength;
if (Str.m_Length < m_Length)
CompareLength = Str.m_Length;
else
CompareLength = m_Length;
Result = memcmp(m_Data,Str.m_Data,CompareLength);
if (Result < 0)
return true;
if (Result > 0)
return false;
if (m_Length < Str.m_Length)
return true;
return false;
}
operator < for strings (from code\string5.cc)
(Figure operator6) This starts out in the same way as our previous version, by figuring out how much of the two strings we actually need to compare character by character. Right after that calculation, though, the code is very different; where's that big for loop?
It's contained in the standard library function memcmp, a carryover from C, which does exactly what that for loop did for us. Although C doesn't have the kind of strings that we're implementing here, it does have primitive facilities for dealing with arrays of characters, including comparing one array with another, character by character. One type of character array supported by C is the C string, which we've already encountered. However, C strings have a serious drawback for our purposes here: They use a null byte to mark the end of a group of characters. This isn't suitable for our strings, whose length is explicitly stored; as noted previously, our strings could theoretically have null bytes in them. There are several C functions that compare C strings, but they rely on the null byte for their proper operation, so we can't use them.
However, these limitations of C strings are so evident that the library writers have supplied another set of functions that act almost identically to the ones used for C strings, except that they don't rely on null bytes to determine how much data to process. Instead, whenever you use one of these functions, you have to tell it how many characters to manipulate. In this case, we're calling memcmp, which compares two arrays of characters up to a specified length. The first argument is the first array to be compared (corresponding to our string), the second argument is the second array to be compared (corresponding to the string Str), and the third argument is the length for which the two arrays are to be compared. The return value from memcmp is calculated by the following rules:
This is very convenient for us, because if the return value from memcmp is less than 0, we know that our result will be true, while if the return value from memcmp is greater than 0, then our result will be false. The only complication, which isn't very complicated, is that if the return value from memcmp is 0, meaning that the two arrays are the same up to the length of the shorter character array, we have to see which is longer. If the first one is shorter, then it precedes the second one; therefore, our result is true. Otherwise, it's false.
Susan had some questions about this version of operator <, including why we had to go through the previous exercise, if we could just use memcmp.
Susan: What is this? I suppose there was a purpose to all the confusing prior discussion if you have an easier way of definingOne small point that shouldn't be overlooked is that in this version of theoperator <? UGH! This new stuff just pops up out of the blue! What is going on? Please explain the reason for the earlier torture.
Steve: I thought it would be better to go through the character-by-character version of
operator <before taking the shortcut. For one thing, that should make it easier to follow the explanation of the "string overrun" problem, as each character comparison shows up in the code.
Susan: So,
memcmpis another library function, and does it stand for memory compare? Also, are the return values are built intomemcmp? This is very confusing, because you have return values in the code.
Steve: Yes,
memcmpstands for "memory compare". As for return values: yes, it has them, but they aren't exactly the ones that we want. We have to return the valuetruefor "less than" andfalsefor "not less than", which aren't the values thatmemcmpreturns. Also,memcmpdoesn't do the whole job when thestrings aren't the same length; in that case, we have to handle the trailing part of the longerstringmanually.
operator < code, we have more than one return statement; in fact, we have four! That's perfectly legal and should be clear to a reader of this function. It's usually not a good idea to scatter return statements around in a large function, because it's easy to overlook them when trying to follow the flow of control through the function. In this case, though, that's not likely to be a problem; any reasonably fluent reader of C++ code will find this organization easy to understand.
Although our current task requires only operator <, another comparison operator, operator ==, will make an interesting contrast in implementation; in addition, a concrete data type that allows comparisons should really implement more than just operator <. Since we've just finished one comparison operator, we might as well knock this one off now (Figure operator7).
This function is considerably simpler than the previous one. Why is this, since they have almost the same purpose? It's because in this case we don't care which of the two strings is greater than the other, just whether they're the same or different. Therefore, we don't have to worry about comparing the two char arrays if they're of different lengths. Two arrays of different lengths can't be the same, so we can just return false. Once we have determined that the two arrays are the same length, we do the comparison via memcmp. This gives us the answer directly, because if Result is 0, then the two strings are equal; otherwise, they're different.
bool string::operator == (const string& Str){
short Result;
if (m_Length != Str.m_Length)
return false;
Result = memcmp(m_Data,Str.m_Data,m_Length);
if (Result == 0)
return true;
return false;
}
operator == for strings (from code\string5.cc)
(Figure operator7) Even though this function is simpler than operator <, it's not simple enough to avoid Susan's probing eye:
Susan: Does==only check to see if the lengths of the arrays are the same? Can it not ever be used for a value?
Steve: It compares the values in the arrays, but only if they are the same length. Since all it cares about is whether they are equal, and arrays of different length can't be equal, it doesn't have to compare the character data unless the arrays are of the same length.
Before moving on to see how we will display a string on the screen via operator <<, I should bring up a couple of points here, because otherwise they might pass you by. First, we didn't have to change our interface header file string5.h (Figure friend1) just because we changed the implementation of operator <. Since the signature of this function didn't change, neither the header file nor the user program had to change. Second, we didn't even implement operator == in the string5a.cc version of the string library and yet our test program still compiled without difficulty. How can this be?
In C++, you can declare all of the functions you want to, whether they are member functions or global functions, without actually defining them. As long as no one tries to actually use the functions, everything will work fine. In fact, the compiler doesn't even care whether any functions you do refer to are available; that's up to the linker to worry about. This is very handy when you know that you're going to add functions in a later revision of a class, as was the case here. Of course, you should warn your class users if you have listed functions in the interface header file that aren't available. It's true that they'll find out about the missing functions the first time they try to link a program that uses one of these functions, because the linker will report that it can't find the function; however, if they've spent a lot of time writing a program using one of these functions, they're likely to get mad at you for misleading them. So let them know what's actually implemented and what's "for later".
Now let's continue with our extensions to the string class, by looking at how we send a string out to the screen.
cout StreamWe've been using cout and its operator << for quite awhile now, but have just taken them for granted. Now we have to look under the hood a bit.
The first question is what type of object cout is. The answer is that it's an ostream (short for "output stream"), which is an object that you can use to send characters to some output device. I'm not sure of the origin of this term, but you can imagine that you push the characters out into a "stream" that leads to the output device.
As you may recall from our uses of cout, you can chain a bunch of << expressions together in one statement, as in Figure cout1. If you compile and execute that program, it will produce the following output:
On test #1, your mark is: A
Notice that it displays the short as a number and the char as a letter, just as we want it to do. This desirable event occurs because there's a separate version of << for each type of data that can be displayed; in other words, operator << uses function overloading, just like the constructors for the StockItem class and the string class. We'll also use function overloading to add support for our string class to the I/O facilities supplied by the iostreams library.
code/cout1.cc
operator << expressions together (code\cout1.cc)
(Figure cout1)
Before we examine how to accomplish this goal, though, we'll have to go into some detail about how the pre-existing output functions behave. Let's start with a simple case using a version of operator << supplied by the iostream.h header file. The simplest possible use of ostream's operator <<, of course, uses only one occurrence of the operator. Here's an example where the value is a char:
cout << 'a';
As you may remember, using an operator such as << on an object is always equivalent to a "normal" function call. This particular example is equivalent to the following:
cout.operator << ('a');
which calls ostream::operator << (char) (i.e.,the version of the operator << member function of the iostream class that takes a char as its input) for the predefined destination cout, which writes the char on the screen.
That takes care of the single occurrence of operator <<. However, as we've already seen, it's possible to string together any number of occurrences of operator <<, with the output of each successive occurrence following the output created by the one to its left. We want our string output function to behave just like the ones predefined in iostream.h, so let's look next at an example that illustrates multiple uses of operator <<, taking a char and a C string:
cout << 'a' << " string";
This is equivalent to
(cout.operator << ('a')).operator << (" string");
What does this mean? Well, since an expression in parentheses is evaluated before one outside the parentheses, the first thing that happens is that ostream::operator << (char) is called for the predefined destination cout, which writes the a to the screen. Now here's the tricky part: the return value from every version of ostream::operator << is a reference to the ostream that it operates on (cout, in this case). Therefore, after the a has been written on the screen, the former expression reduces to this:
cout.operator << (" string");
That is, the next output operation behaves exactly like the first one. In this case, ostream::operator << (char*) is the function called, because char* is the type of the argument to be written out. It too returns a reference to the ostream for which it was called, so that any further << calls can add their data to that same ostream. It should be fairly obvious how the same process can be extended to handle any number of items to be displayed.
That illustrates how the designers of ostream could create member functions that would behave in this convenient way. However, we can't use the same mechanism that they did; we can't modify the definition of the ostream class in the library, because we didn't write it in the first place and don't have access to its source code.8 Does that mean that we can't give our strings convenient input and output facilities?
In fact, we can. To do this, we create a global function called operator << that accepts an ostream& (that is, a reference to an ostream), adds the contents of our string to the ostream, and then returns a reference to the same ostream. This will support multiple occurrences of operator << to be chained together in one statement, just as with the operator << member functions from the iostreams library. We can see the implementation of this function in Figure cout2.
ostream& operator << (ostream& s, const string& Str){
short i;
for (i=0; i < Str.m_Length-1; i ++)
S << Str.m_Data[i];
return S;
}
operator << function to output a string (from code\string5.cc)
(Figure cout2) As usual, we should first examine the function declaration; in this case, a couple of points are worth noting. We've already seen that the first argument is an ostream&, to which we will add the characters from the string that's the second argument. Also notice that the second argument is a const string&, that is, a reference to a constant string. This is the best way to declare this argument because we aren't going to change the argument, and there's no reason to make a copy of it.
But possibly the most important point about the function declaration is that this operator << is not a member function of the string class, which explains why it isn't called string::operator <<. It's a global function that can be called anywhere in a program that needs to use it, so long as that program has included the header file that defines it. Its operation is pretty simple: It simply calls ostream::operator << (char) to write out each character from the array called m_Data that we use to store the data for our string. Since there is no ostream function to write out a specified number of characters from a char array, we have to call ostream::operator << (char) for each character in the array.9 After all the characters have been written to the ostream, we return it so that the next operator << call in the line can continue producing output.
However, there's a loose end here. How can a global function, which by definition isn't a member function of class string, get at the internal workings of a string? We declared that m_Length and m_Data were private, so that they wouldn't be accessible to just any old function that wandered along to look at them. Is nothing sacred?
In fact, private data aren't accessible to just any function. However, operator << (ostream&, const string&) isn't just any function. Take a look at string5.h in Figure friend1 to see why. The line we're interested in here is this one:
friend ostream& operator << (ostream& s, const string& Str);
The key word here is friend. We're telling the compiler that a function with the signature ostream& operator << (ostream&, const string&) is permitted to access the information normally reserved for member functions of the string class; that is, anything that isn't marked public. It's possible to make an entire class a friend to another class; here, we're specifying one function that is a friend to this class.10 You probably won't be surprised to learn that Susan had some questions about this operator. Let's see how the discussion went:
Susan: Let's start withfriend. . . what is that?
Steve: A
friendis a function orclassthat is allowed to access internals of thisclass, as though thefriendwere a member function. In other words, theprivateaccess specifier doesn't have any effect onfriends.
Susan: What is an
ostream?
Steve: An
ostreamis astreamthat is used for output;streams can be either input (istream) or output (ostream).
Susan: This
streamcharacter seems to have a lot of relatives.
Steve: You're right; there are lots of
classes in the stream family, includingistream,ostream,ifstream, andofstream.
That explains why this global function can access our non-public data. But why did we have to create a global function in the first place, rather than just adding a member function to our string class?
Because a member function of a class has to be called for an object of that class, whose address then becomes the this pointer; in the case of the << operator, the class of the object is ostream, not string. Figure whyglobal is an example.
string x = "this is it";
cout << x;
operator <<
(Figure whyglobal)
The line cout << x; is the same as cout.operator << (x);. Notice that the object to which the operator << call is applied is cout, not x. Since cout is an ostream, not a string, we can't use a member function of string to do our output, but a global function is perfectly suitable.
Before we move on to our next topic, one little point in the implementation of operator << for strings could use some more explanation: Why is the loop continuation expression i < Str.m_Length-1;? The reason is that the stored length of the string (m_Length) includes the added null byte at the end of the string; thus, if we write out all the bytes indicated by the length, we'll include the null byte as well. This would work all right if we were writing the data out to the screen, because we can't read the data back from the screen. However, it would cause trouble if we wrote the data to a file and then tried to reread the data later, as we did in the StockItem class. Therefore, we have to be careful to avoid writing the null byte. Of course, if we had a way to make a string that had null bytes inside it rather than just at the end, then we'd really have to deal with the problem of handling null bytes during input and output. However, in that case we would presumably have to deal with all the other ramifications of such strings, and this would be just another detail to handle. For now, it's best to avoid it in the interest of simplicity.
Now that we have an output function that will write our string variables out to an ostream like cout, it would be very handy to have an input function that could read a string in from an istream like cin. You might expect that this would be pretty simple now that we've worked through the previous exercise, and you'd be mostly right. As usual, though, there are a few twists in the path. Let's start by looking at the code in Figure operatorrshift.
istream& operator >> (istream& s, string& Str){
const short BUFLEN = 256;
char Buf[BUFLEN];
memset(Buf,0,BUFLEN);
if (S.peek() == '\n')
S.ignore();
S.getline(Buf,BUFLEN,'\n');
Str = Buf;
return S;
}
operator >> function to input a string (from code\string5.cc)
(Figure operatorrshift) The header is pretty similar to the one from the operator << function, which is reasonable, since they're complementary functions. In this case, we're defining a global function with the signature istream& operator >> (istream& s, string& Str). In other words, this function, called operator >>, has a first argument that is a reference to an istream, which is just like an ostream except that we read data from it rather than writing data to it. One significant difference between this function signature and the one for operator << is that the second argument is a non-const reference, rather than a const reference, to the string into which we want to read the data from the istream. That's because the whole purpose of this function is to modify the string passed in as the second argument; to be exact, we're going to fill it in with the characters taken out of the istream.
Continuing with the analysis of the function declaration, the return value is another istream reference, which is passed to the next operator >> function to the right, if there is one; otherwise it will just be discarded.
After decoding the header, let's move to the first line in the function body, const short BUFLEN = 256;. While we've encountered const before, specifying that we aren't going to change an argument passed to us, that can't be the meaning here. What does const mean in this context?
It specifies that the item being defined, which in this case is short BUFLEN, isn't a variable, but a constant, or const value.11 That is, its value can't be changed. Of course, a logical question is how we can use a const, if we can't set its value.
This is another of the places where it's important to differentiate between initialization and assignment. We can't assign a value to a const, but we can initialize it; in fact, because an uninitialized const is useless, the attempt to define a const without specifying its initial value is a compile-time error. In this case, we're initializing it to the value 256; if we just wrote const short BUFLEN;, we'd get an error something like the one in Figure uninit when we tried to compile it.
code/string5x.out
const (code\string5x.out)
(Figure uninit)Susan wanted some further explanation.
Susan: I still don't get whyNow that we've disposed of that detail, let's continue with our examination of the implementation ofconstis used here.
Steve: This is a different use of
constthan we've seen before; in this case, it's an instruction to the compiler meaning "the following 'variable' isn't really variable, but constant. Don't allow it to be modified." This allows us to use it where we would otherwise have to use a literal constant, like 256 itself. The reason that using aconstis better than using a literal constant is that it makes it easier to change all the occurrences of that value. In the present case, for example, we useBUFLENthree times after its definition; if we used the literal constant 256 in all of those places, we'd have to change all of them if we decided to make the buffer larger or smaller. As it is, however, we only have to change the definition ofBUFLEN, and all of the places where it's used will use the new value automatically.
Susan: Okay, I think I have it now.
operator >>. The next nonblank line is char Buf[BUFLEN];. This is a little different from any variable definition we've seen before; however, you might be able to guess something about it from its appearance. It seems to be defining a variable called Buf12 whose type is related in some way to char. But what about the [BUFLEN] part?
This is a definition of a variable of that dreaded type, the array; specifically, we're defining an array called Buf, which contains BUFLEN chars. As you may recall, this is somewhat like the vector type that we've used before, except that it has absolutely no error checking; if we try to access a char that is past the end of the array, something will happen, but not anything good. In this case, as in our previous use of pointers, we'll use this dangerous construct only in a very small part of our code, under controlled circumstances; the user of our string class won't be exposed to the array. We'll see how it's used in this function.
First, though, I should point out that C++ has a rule that the number of elements of an array must be known at compile time. That is, the program in Figure nonconst1 isn't legal C++.
code/string5y.cc
const array size (code\string5y.cc)
(Figure nonconst1) I'll admit that I don't understand exactly why using a non-const array size is illegal; a C++ compiler has enough information to create and access an array whose length is known at run time.13 In fact, the DJGPP compiler supplied with this book by default does accept this construct: You have to set a special warning option (pedantic-errors) to treat this as an error. I've added this option to the settings in RHIDE. Figure nonconst2 shows the output that you would get if you used RHIDE to compile the program in Figure nonconst1 with that setting in force.
code/string5y.out
const array size (code\string5y.out)
(Figure nonconst2)Although the ability to declare an array whose size isn't known until run time is sometimes very convenient and is provided by the DJGPP compiler, you should avoid it. No other compiler I'm familiar with will accept this construct; it also won't be part of the C++ standard, so when such compilers are available, they won't accept it either.
Therefore, we'll use the const value BUFLEN to specify the number of chars in the array Buf in the statement char Buf[BUFLEN];.
Now we're up to the first line of the executable part of the operator >> function in Figure operatorrshift: memset(Buf,0,BUFLEN);. This is a call to a function called memset, which is in the standard C library. You may be able to guess from its name that it is related to the function memcmp that we used to compare two arrays of chars. If so, your guess would be correct; memset is C-talk for "set all the bytes in an area of memory to the same value". The first argument is the address of the area of memory to be set to the value, the second argument is the char value to be used, and the third argument is the number of characters to be set to that value, starting at the address given in the first argument. In other words, we're setting all of the characters in the array called Buf to 0. This is important because we're going to treat that array as a C string later. As you may recall, a C string is terminated by a null byte, so we want to make sure that no junk is lying around in the array Buf, which would be misinterpreted as part of the data we're reading in from the istream.
Next, we have an if statement:
if (S.peek() == '\n')
S.ignore();
What exactly does this do? It solves a problem with reading C string data from a file; namely, where do we stop reading? With a numeric variable, that's easy; the answer is "whenever we see a character that doesn't look like part of a number". However, with a data type that can take just about any characters as part of its value, this is more difficult. The solution I've adopted is to stop reading when we get to a newline ('\n') character; that is, an end-of-line character. This is no problem when reading from the keyboard, as long as each data item is on its own line, but what about reading from a file?
When we read a C string from a file, the newline at the end of the line is discarded, so the next C string to be read in starts at the beginning of the next line of the file, as we wish. This approach to handling newline characters works well as long as all of the variables being read in are strings. However, in the case of the StockItem class, we needed to be able to mix shorts and strings in the file. In that case, reading a value for a short stops at the newline, because that character isn't a valid part of a numeric value. This is OK as long as the next variable to be read is also a short, because spaces and newlines at the beginning of a line are ignored when we're reading a numeric value. However, when the next variable to be read is a string, the leftover newline from the previous read is interpreted as the beginning of the data for the string, which messes up everything. Therefore, we have to check whether the next available char in the input stream is a newline, in which case we have to skip it. On the other hand, if the next character to be read in is something other than a newline, we want to keep it as the first character of our string. That's what the if statement does: First, the S.peek() function call returns the next character in the input stream without removing it from the stream; then, if it turns out to be a newline, we tell the input stream to ignore it, so it won't mess up our reading of the actual data in the next line.
You won't be surprised to hear that Susan had a couple of questions about this function.
Susan: Where doNow that we've dealt with that detail, we're ready to read the data for ourpeekandignorecome from?
Steve: They're defined in the
iostreamsheader fileiostream.h.
Susan: How did you know that they were available?
Steve: By reading a book called C++ IOstreams Handbook by Steve Teale.
string. That's the job of the next line in the function: S.getline(Buf,BUFLEN,'\n');. Since s is an istream, this is a member function of istream. To be precise, it's the member function that reads a number of characters into a char array. The arguments are as follows:
getline should stop reading characters
This function will read characters into the array (in this case Buf) until one of two events occurs:
Note that the terminating character is not read into the array.
Before continuing with the rest of the code for operator >>, let's take a closer look at the following two lines, so we can see why it's a bad idea to use the C string and memory manipulation library any more than we have to. The lines in question are
memset(Buf,0,BUFLEN);
S.getline(Buf,BUFLEN,'\n');
The problem is that we have to specify the length of the array Buf explicitly (as BUFLEN, in this case). In this small function, we can keep track of that length without much effort, but in a large program with many references to Buf, it would be all too easy to make a mistake in specifying its length. As we've already seen, the result of specifying a length that is greater than the actual length of the array would be a serious error in the functioning of the program; namely, some memory belonging to some other variable would be overwritten. Whenever we use the mem functions in the C library, we're liable to run into such problems. That's an excellent reason to avoid them except in strictly controlled situations, such as the present one, where the definition of the array is in the same small function as the uses of the array. By no coincidence, this is the same problem caused by the indiscriminate use of pointers; the difficulty with the C memory manipulation functions is that they use pointers (or arrays, which are essentially interchangeable with pointers), with all of the hazards that such use entails.
Now that I've nagged you sufficiently about the dangers of arrays, let's look at the rest of the operator >> code. The next statement is Str = Buf;, which sets the argument Str to the contents of the array Buf. Buf is the address of the first char in an array of chars, so its type is char*; Str, on the other hand, is a string. Therefore, this apparently innocent assignment statement actually calls string::string(char*) to make a temporary string, and then calls string::operator=(const string&) to copy that temporary string to Str.
Finally, we have the statement return S;. This simply returns the same istream that we got as an argument, so that the next input operator in the same statement can continue reading from the istream where we left off. Now our strings can be read in from an input stream (such as cin) and written out to an output stream (such as cout), so our program that sorts strings can do some useful work.14
Assuming that you've installed the software from the CD-ROM in the back of this book, you can try out this program. First, you have to compile it by changing to the code subdirectory under the main directory where you installed the software, and typing RHIDE strsort1, then using the "Make" command from the "Compile" menu. Then exit back to DOS and type strsort1 to run the program.
To run it under the debugger, make sure you are in the code subdirectory, and then type "RHIDE strsort1". Again, do *not* add the ".cc" to the end of the file name. Once RHIDE has started up, you can step through the program by hitting F8, which will treat any function call as one statement, or by hitting F7, which will step into any function call. Any time you want to see the display that the user would see when running the program normally, hit Alt-F5, then ENTER to get back to the debugger.
Now that we've finished our improvements to the string class, it's time to look back at what we've covered since our first review in this chapter.
After finishing up the requirements to make the string class a concrete data type, we continued to add more facilities to the string class; to be precise, we wanted to make it possible to modify the sorting program of Chapter to handle strings rather than shorts. To do this, we had to be able to compare two strings to determine which of the two would come first in the dictionary and to read strings from an input stream (like cin) and write them to an output stream (like cout). Although the Display function provided a primitive mechanism for writing a string to cout, it's much nicer to be able to use the standard >> and << operators that can handle all of the native types, so we resolved to make those available for strings as well.
We started out by implementing the < operator so that we could compare two strings x and y to see which would come before the other in the dictionary, simply by writing if (x < y). The implementation of this function turned out to be a bit complicated because of the possibility of "running off the end" of one of the strings, when the strings are of different lengths.
Once we worked out the appropriate handling for this situation, we examined two implementations of the algorithm for operator <. The first compared characters from the two strings one at a time, while the second used memcmp, a C function that compares two sets of bytes and returns a different value depending on whether the first one is "less than", "equal to", or "greater than" the second one, using dictionary ordering to make this determination.
Then we developed an implementation of operator == for strings, which turned out to be considerably simpler than the second version of operator <, even though both functions used memcmp to do most of the work; the reason is that we have to compare the contents of the strings only if they are of the same length, because strings of different lengths are not equal.
Then we started looking beneath the covers of the output functions <<, starting with the predefined versions of << that handle char and C string arguments. The simplest case of using this operator, of course, is to display one expression on the screen via cout. Next, we examined the mechanism by which several uses of this operator can be chained together to allow the displaying of a number of expressions with one statement.
The next question was: How could we provide these handy facilities for the users of our string class? Would we have to modify the ostream classes to add support for strings? Luckily, the designers of the stream classes were foresightful enough to enable us to add support for our own data types without having to modify their code. The key is to create a global function that can add the contents of our string to an existing ostream variable and pass that ostream variable on to the next possible user, just as in the chaining mentioned previously for native types.
The implementation of this function wasn't terribly complicated; it merely wrote each char of the string's data to the output stream. The unusual part of this function was that it wasn't a member function of string, but a global function, as is needed to maintain the same syntax as the output of native types. We used the friend specifier to allow this version of operator << access to private members of string such as m_Length and m_Data.
After we finished the examination of our version of operator << for sending strings to an ostream, we went through the parallel exercise of creating a version of operator >> to read strings from an istream. This turned out to be a bit more complicated, since we had to make room for the incoming data, which limited the maximum length of string that we could read in. In the process of defining this maximum length, we also encountered a new construct, the const value. This is a data item that is declared just like a variable, except that its value is initialized once and cannot be changed, which makes it ideal for specifying a constant size for an array, a constant loop limit, or other value that doesn't change from one execution of the program to the next. Next, we used this const value to declare an array of chars to hold the input data to be stored in the string, and filled the array with null bytes, by calling the C function memset. We followed this by using some member functions of the istream class to eliminate any newline ('\n') character that might have been left over from a previous input operation.
Finally, we were ready to read the data into the array of chars, in preparation for assigning it to our string. After doing that assignment, we returned the original istream to the caller, to allow chaining of operations as is standard with operator << and operator >>.
That completes the review of this chapter. Now let's do some exercises to help it all sink in.
1. What would happen if we compiled the program in Figure strex6? Why?
code/strex6.cc
2. We have already implemented operator < and operator ==. However, a concrete data type that allows for ordered comparisons such as < should really implement all six of the comparison operators. The other four of these operators are >, >=, <=, and != ("greater than", "greater than or equal to", "less than or equal to", and "not equal to", respectively). Add the declarations of each of these operators to the string interface definition.
3. Implement the four comparison operators that you declared in the previous exercise.
4. Write a test program to verify that all of the comparison operators work. This program should test that each of the operators returns the value true when its condition is true; equally important, it should test that each of the operators returns the value false when the condition is not true.
In this chapter, we have significantly improved the string class, learning some generally useful techniques and lessons in the process. Now it's time to wrap up this book, so once you finish doing the exercises,15 turn to Appendix , where we'll tie up some loose ends, including some indications of your likely future path in learning more about C++.
1. The output of the compiler should look something like this:
strex6.cc: In function `int main()':
strex6.cc:9: constructor `string::string(char *)' is private
strex6.cc:21: within this context
strex6.cc:6: in passing argument 1 of `string::operator =(const string &)'
This one is a bit tricky. The actual problem is that making the constructor string::string(char*) private prevents the automatic conversion from char* to string required for the string::operator = (const string&) assignment operator to work. As long as there is an accessible string::string(char*) constructor, the compiler will use that constructor to build a temporary string from a char* argument on the right side of an =. This temporary string will then be used by string::operator = (const string&) as the source of data to modify the string on the left of the =. However, this is not possible if the constructor that makes a string from a char* isn't accessible where it is needed.16
2. The new class interface is shown in Figure string6.h.
string class interface file (from code\string6.h)
(Figure string6.h)#include <iostream.h>
class string
{
friend ostream& operator << (ostream& s, const string& Str);
friend istream& operator >> (istream& s, string& Str);
public:
string();
string(const string& Str);
string& operator = (const string& Str);
~string();
string(char* p);
short GetLength();
bool operator < (const string& Str);
bool operator == (const string& Str);
bool operator > (const string& Str);
bool operator >= (const string& Str);
bool operator <= (const string& Str);
bool operator != (const string& Str);
private:
short m_Length;
char* m_Data;
};
3. The implementations of the comparison operators are shown in Figure operatorgt through Figure strcmp-1.
string class implementation of operator > (from code\string6.cc)
(Figure operatorgt)bool string::operator > (const string& Str){
short Result;
short CompareLength;
if (Str.m_Length < m_Length)
CompareLength = Str.m_Length;
else
CompareLength = m_Length;
Result = memcmp(m_Data,Str.m_Data,CompareLength);
if (Result > 0)
return true;
if (Result < 0)
return false;
if (m_Length > Str.m_Length)
return true;
return false;
}
string class implementation of operator >= (from code\string6.cc)
(Figure operatorge)bool string::operator >= (const string& Str){
short Result;
short CompareLength;
if (Str.m_Length < m_Length)
CompareLength = Str.m_Length;
else
CompareLength = m_Length;
Result = memcmp(m_Data,Str.m_Data,CompareLength);
if (Result > 0)
return true;
if (Result < 0)
return false;
if (m_Length >= Str.m_Length)
return true;
return false;
}
string class implementation of operator != (from code\string6.cc)
(Figure operatorne)bool string::operator != (const string& Str){
short Result;
if (m_Length != Str.m_Length)
return true;
Result = memcmp(m_Data,Str.m_Data,m_Length);
if (Result == 0)
return false;
return true;
}
string class implementation of operator <= (from code\string6.cc)
(Figure operatorle)bool string::operator <= (const string& Str){
short Result;
short CompareLength;
if (Str.m_Length < m_Length)
CompareLength = Str.m_Length;
else
CompareLength = m_Length;
Result = memcmp(m_Data,Str.m_Data,CompareLength);
if (Result < 0)
return true;
if (Result > 0)
return false;
if (m_Length <= Str.m_Length)
return true;
return false;
}
4. The test program appears in Figure strcmp.
string class comparison operators (code\strcmp.cc)
(Figure strcmp)#include <iostream.h>#include "string6.h"
int main()
{
string x = "x";
string xx = "xx";
string y = "y";
string yy = "yy";
// testing <
if (x < x)
cout << "ERROR: x < x" << endl;
else
cout << "OKAY: x NOT < x" << endl;
if (x < xx)
cout << "OKAY: x < xx" << endl;
else
cout << "ERROR: x NOT < xx" << endl;
if (x < y)
cout << "OKAY: x < y" << endl;
else
cout << "ERROR: x NOT < y" << endl;
// testing <=
if (x <= x)
cout << "OKAY: x <= x" << endl;
else
cout << "ERROR: x NOT <= x" << endl;
if (x <= xx)
cout << "OKAY: x <= xx" << endl;
else
cout << "ERROR: x NOT <= xx" << endl;
if (x <= y)
cout << "OKAY: x <= y" << endl;
else
cout << "ERROR: x NOT <= y" << endl;
// testing >
if (y > y)
cout << "ERROR: y > y" << endl;
else
cout << "OKAY: y NOT > y" << endl;
if (yy > y)
cout << "OKAY: yy > y" << endl;
else
cout << "ERROR: yy NOT > y" << endl;
if (y > x)
cout << "OKAY: y > x" << endl;
else
cout << "ERROR: y NOT > x" << endl;
// testing >=
if (y >= y)
cout << "OKAY: y >= y" << endl;
else
cout << "ERROR: y NOT >= y" << endl;
if (yy >= y)
cout << "OKAY: yy >= y" << endl;
else
cout << "ERROR: yy NOT >= y" << endl;
if (y >= x)
cout << "OKAY: y >= x" << endl;
else
cout << "ERROR: y NOT >= x" << endl;
// testing ==
if (x == x)
cout << "OKAY: x == x" << endl;
else
cout << "ERROR: x NOT == x" << endl;
if (x == xx)
cout << "ERROR: x == xx" << endl;
else
cout << "OKAY: x NOT == xx" << endl;
if (x == y)
cout << "ERROR: x == y" << endl;
else
cout << "OKAY: x NOT == y" << endl;
// testing !=
if (x != x)
cout << "ERROR: x != x" << endl;
else
cout << "OKAY: x NOT != x" << endl;
if (x != xx)
cout << "OKAY: x != xx" << endl;
else
cout << "ERROR: x NOT != xx" << endl;
if (x != y)
cout << "OKAY: x != y" << endl;
else
cout << "ERROR: x NOT != y" << endl;
return 0;
}
operator =, Figure strnsmem1.
strings and C strings as I've done in previous diagrams, I'm just showing the address of the first character in each group, so that the figure will fit on one page.
operator = function isn't going to modify an argument with the const specifier; if we wrote an operator = that tried to modify such an argument, it wouldn't compile.
m_Length member variable and just have the GetLength call strlen to figure out the length; thus, the user's source code wouldn't have to be changed just because we changed the way we keep track of the length.
strings longer than 32767 bytes, we would have to change the return type of GetLength to something more capacious than an short. However, we still have a lot more leeway to make changes in the implementation than we would have if we allowed direct access to our member variables.
||: if the expression on the left is false, then the answer must be false and the expression on the right is not executed at all. The reason for this short-circuit evaluation rule is that in some cases you may want to write a right-hand expression that will only be legal if the left-hand expression is true.
ostream class, we wouldn't want to modify it, for a number of reasons. One excellent reason is that every time a new version of the library came out, we'd have to make our changes again. Also, there are other ways to reuse the code from the library for our own purposes; unfortunately, these mechanisms are beyond the scope of this book.
ostream::operator <<(char) here, it's because s is an ostream&, which is just another name for the ostream that is the first argument to this function.
friend declaration would not permit a function with the same name and a different signature, for example ostream& operator << (ostream&, int) to access non-public members of string.
BUFLEN, it's short for "buffer length". Also, I should mention the reason that it is all caps rather than mixed case or all lower case: An old C convention (carried over into C++) specifies that named constants should be named in all caps to enable the reader to distinguish them from variables at a glance.
operator << will also work for any other output destination, such as a file; however, our current implementation of operator >> isn't really suitable for reading a string in from an arbitrary input source. The reason is that we're counting on the input data being able to fit into the Buf array, which is 256 bytes in length. This is fine for input from the keyboard, at least under DOS, because the maximum line length in that situation is 128 characters. It will also work for our inventory file, because the lines in that file are shorter than 256 bytes. However, there's no way to limit the length of lines in any old data file we might want to read from, so this won't do as a general solution. Of course, increasing the size of the Buf array wouldn't solve the problem; no matter how large we make it, we couldn't be sure that a line from a file wouldn't be too long. The solution would be to handle long lines in sections; unfortunately, we don't have all of the infrastructure needed to implement that version of operator>>.
char * means, it's the same as char*. As I've mentioned previously, I prefer the latter as being easier to understand, but they mean the same to the compiler.